/*
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */

package org.evosuite.instrumentation.error;

import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * <p>
 * ErrorConditionChecker class.
 * </p>
 *
 * @author fraser
 */
public class ErrorConditionChecker {

    private static final Logger logger = LoggerFactory.getLogger(ErrorConditionChecker.class);

    /**
     * <p>
     * scale
     * </p>
     *
     * @param value a float.
     * @return a int.
     */
    public static int scale(float value) {
        return (Integer.MAX_VALUE - 2) * (int) Math.ceil((value / (value + 1.0F)));
    }

    /**
     * <p>
     * scale
     * </p>
     *
     * @param value a double.
     * @return a int.
     */
    public static int scale(double value) {
        return (Integer.MAX_VALUE - 2) * (int) Math.ceil((value / (value + 1.0)));
    }

    /**
     * <p>
     * scale
     * </p>
     *
     * @param value a long.
     * @return a int.
     */
    public static int scale(long value) {
        return (Integer.MAX_VALUE - 2) * (int) Math.ceil((value / (value + 1.0)));
    }

    public static int scaleTo(double value, int max) {
        return (int) (Math.ceil(max * (1.0 * value / (value + 1.0))));
    }

    /**
     * <p>
     * overflowDistance
     * </p>
     *
     * @param op1    a int.
     * @param op2    a int.
     * @param opcode a int.
     * @return a int.
     */
    public static int overflowDistance(int op1, int op2, int opcode) {
        switch (opcode) {

            case Opcodes.IADD:
                int result = overflowDistanceAdd(op1, op2);
                logger.debug("O: " + op1 + " + " + op2 + " = " + (op1 + op2) + " -> " + result);
                return result;

            case Opcodes.ISUB:
                return overflowDistanceSub(op1, op2);

            case Opcodes.IMUL:
                return overflowDistanceMul(op1, op2);

            case Opcodes.IDIV:
                return overflowDistanceDiv(op1, op2);
        }
        return Integer.MAX_VALUE;
    }

    protected final static int HALFWAY = Integer.MAX_VALUE / 2;

    protected static int overflowDistanceAdd(int op1, int op2) {
        int result = op1 + op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            if (result < 0)
                return result;
            else {
                int retVal = HALFWAY - scaleTo(result, HALFWAY);
                if (retVal != 0)
                    return retVal;
                else
                    return 1;
            }
        } else if (op1 < 0 && op2 < 0) {
            // if both are negative then both need to be increased
            return HALFWAY
                    + scaleTo(Math.abs((long) op1) + Math.abs((long) op2), HALFWAY);
        } else if (op1 >= 0 && op2 < 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceAdd(int op1, int op2) {
        int result = op1 + op2;
        if (op1 <= 0 && op2 <= 0) {
            if (op1 == Integer.MIN_VALUE && op2 == Integer.MIN_VALUE) {
                return Integer.MIN_VALUE;
            } else {
                // result has to be < 0 for overflow
                return result > 0 ? -result : HALFWAY
                        - scaleTo(Math.abs((long) result), HALFWAY) + 1;
            }
        } else if (op1 > 0 && op2 > 0) {
            // if both are positive then both need to be decreased
            return HALFWAY
                    + scaleTo(Math.abs((long) op1) + Math.abs((long) op2), HALFWAY);
        } else if (op1 >= 0 && op2 < 0) {
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else {
            // Unreachable
            return Integer.MAX_VALUE;
        }
    }

    protected static int overflowDistanceSub(int op1, int op2) {
        int result = op1 - op2;
        if (op1 >= 0 && op2 <= 0) {
            // result has to be < 0 for overflow
            return result < 0 ? result : HALFWAY + 1 - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            // if both are negative then an overflow will be difficult
            return HALFWAY
                    + scaleTo(Math.abs((long) op1) + Math.abs((long) op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs((long) op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceSub(int op1, int op2) {
        int result = op1 - op2;
        if (op1 <= 0 && op2 >= 0) {
            return result > 0 ? -result : HALFWAY + 1 - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 < 0) {
            return HALFWAY
                    + scaleTo(Math.abs((long) op1) + Math.abs((long) op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(op1, HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs((long) op2), HALFWAY);
        } else {
            // Not sure if this can be reached
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int overflowDistanceMul(int op1, int op2) {
        int result = op1 * op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            // the result can be so large that it overflows so much to be positive again
            // so we need to use longs to check this
            long longResult = (long) op1 * (long) (op2);
            if (longResult > Integer.MAX_VALUE) {
                if (result <= 0)
                    return result;
                else
                    return Integer.MIN_VALUE;
            } else {
                int retval = HALFWAY - scaleTo(result, HALFWAY);
                if (retval > 0)
                    return retval;
                else
                    return 1;
            }
            //System.out.println(op1+" * "+op2 +" -> "+result +" -> "+(HALFWAY - scaleTo(result, HALFWAY)));
            //return result <= 0 ? result : HALFWAY - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 < 0) {
            return result <= 0 ? result : HALFWAY - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 < 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int underflowDistanceMul(int op1, int op2) {
        int result = op1 * op2;
        if (op1 > 0 && op2 < 0) {
            return result >= 0 ? -result : HALFWAY - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return result >= 0 ? -result : HALFWAY - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 > 0) {
            return HALFWAY + scaleTo(Math.min(op1, op2), HALFWAY);
        } else if (op1 < 0 && op2 < 0) {
            return HALFWAY + scaleTo(Math.abs(Math.max(op1, op2)), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int overflowDistanceDiv(int op1, int op2) {
        if (op1 == Integer.MIN_VALUE && op2 == -1)
            return -1;
        else {
            // If op2 is MAX_VALUE then -1 -op2 will give us an overflow
            if (op2 == Integer.MAX_VALUE)
                return Integer.MAX_VALUE;

            // TODO There may be an overflow here
            return scaleTo(Math.abs(Integer.MIN_VALUE - op1), HALFWAY)
                    + scaleTo(Math.abs(-1 - op2), HALFWAY);
        }
    }

    public static int underflowDistance(int op1, int op2, int opcode) {
        switch (opcode) {

            case Opcodes.IADD:
                int result = underflowDistanceAdd(op1, op2);
                logger.debug("U: " + op1 + " + " + op2 + " = " + (op1 + op2) + " -> " + result);
                return result;

            case Opcodes.ISUB:
                return underflowDistanceSub(op1, op2);

            case Opcodes.IMUL:
                return underflowDistanceMul(op1, op2);

        }
        return Integer.MAX_VALUE;
    }

    public static int overflowDistance(float op1, float op2, int opcode) {
        switch (opcode) {

            case Opcodes.FADD:
                return overflowDistanceAdd(op1, op2);

            case Opcodes.FSUB:
                return overflowDistanceSub(op1, op2);

            case Opcodes.FMUL:
                return overflowDistanceMul(op1, op2);

            case Opcodes.FDIV:
                return overflowDistanceDiv(op1, op2);
        }
        return Integer.MAX_VALUE;
    }

    public static int underflowDistance(float op1, float op2, int opcode) {
        switch (opcode) {

            case Opcodes.FADD:
                return underflowDistanceAdd(op1, op2);

            case Opcodes.FSUB:
                return underflowDistanceSub(op1, op2);

            case Opcodes.FMUL:
                return underflowDistanceMul(op1, op2);

        }
        return Integer.MAX_VALUE;
    }

    protected static int overflowDistanceAdd(float op1, float op2) {
        float result = op1 + op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            return result == Float.POSITIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY) + 1;

        } else if (op1 < 0 && op2 < 0) {
            // if both are negative then both need to be increased
            return result == Float.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo((double) op1 + (double) op2, HALFWAY);
        } else if (op1 >= 0 && op2 < 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceAdd(float op1, float op2) {
        float result = op1 + op2;
        if (op1 <= 0 && op2 <= 0) {
            // result has to be < 0 for overflow
            return result == Float.NEGATIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(Math.abs((double) result), HALFWAY) + 1;
        } else if (op1 > 0 && op2 > 0) {
            // if both are positive then both need to be decreased
            return result == Float.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs((double) op1) + Math.abs((double) op2), HALFWAY);
        } else if (op1 >= 0 && op2 < 0) {
            return HALFWAY + scaleTo(op1, HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else {
            // Unreachable
            return Integer.MAX_VALUE;
        }
    }

    protected static int overflowDistanceSub(float op1, float op2) {
        float result = op1 - op2;
        if (op1 >= 0 && op2 <= 0) {
            // result has to be < 0 for overflow
            return result == Float.POSITIVE_INFINITY ? -1 : HALFWAY + 1
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            // if both are negative then an overflow will be difficult
            return result == Float.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs((double) op1) + Math.abs((double) op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs((double) op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceSub(float op1, float op2) {
        float result = op1 - op2;
        if (op1 <= 0 && op2 >= 0) {
            return result == Float.NEGATIVE_INFINITY ? -1 : HALFWAY + 1
                    - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 < 0) {
            return result == Float.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs((double) op1) + Math.abs((double) op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(Math.abs((double) op1), HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs((double) op2), HALFWAY);
        } else {
            // Not sure if this can be reached
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int overflowDistanceMul(float op1, float op2) {
        float result = op1 * op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            return result == Float.POSITIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 < 0) {
            return result == Float.POSITIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY) + 1;
        } else if (op1 > 0 && op2 < 0) {
            // In this case we can't have an overflow yet
            return result == Float.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return result == Float.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int underflowDistanceMul(float op1, float op2) {
        float result = op1 * op2;
        if (op1 > 0 && op2 < 0) {
            return result == Float.NEGATIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return result == Float.NEGATIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 > 0) {
            return result == Float.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.min(op1, op2), HALFWAY);
        } else if (op1 < 0 && op2 < 0) {
            return result == Float.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(Math.max(op1, op2)), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int overflowDistanceDiv(float op1, float op2) {
        if (op1 == -Float.MAX_VALUE && op2 == -1.0)
            return -1;
        else
            // TODO There may be an overflow here
            return scaleTo(Math.abs(-Float.MAX_VALUE - op1), HALFWAY)
                    + scaleTo(Math.abs(-1.0 - op2), HALFWAY);
    }

    public static int overflowDistance(double op1, double op2, int opcode) {
        switch (opcode) {

            case Opcodes.DADD:
                return overflowDistanceAdd(op1, op2);

            case Opcodes.DSUB:
                return overflowDistanceSub(op1, op2);

            case Opcodes.DMUL:
                return overflowDistanceMul(op1, op2);

            case Opcodes.DDIV:
                return overflowDistanceDiv(op1, op2);
        }
        return Integer.MAX_VALUE;
    }

    public static int underflowDistance(double op1, double op2, int opcode) {
        switch (opcode) {

            case Opcodes.DADD:
                return underflowDistanceAdd(op1, op2);

            case Opcodes.DSUB:
                return underflowDistanceSub(op1, op2);

            case Opcodes.DMUL:
                return underflowDistanceMul(op1, op2);

        }
        return Integer.MAX_VALUE;
    }

    protected static int overflowDistanceAdd(double op1, double op2) {
        double result = op1 + op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            return result == Double.POSITIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY) + 1;

        } else if (op1 < 0 && op2 < 0) {
            return result == Double.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    - scaleTo(result, HALFWAY) + 1;
        } else if (op1 >= 0 && op2 < 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceAdd(double op1, double op2) {
        double result = op1 + op2;
        if (op1 <= 0 && op2 <= 0) {
            // result has to be < 0 for overflow
            return result == Double.NEGATIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(Math.abs(result), HALFWAY) + 1;
        } else if (op1 > 0 && op2 > 0) {
            // if both are positive then both need to be decreased
            return result == Double.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1) + Math.abs(op2), HALFWAY);
        } else if (op1 >= 0 && op2 < 0) {
            return HALFWAY + scaleTo(op1, HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else {
            // Unreachable
            return Integer.MAX_VALUE;
        }
    }

    protected static int overflowDistanceSub(double op1, double op2) {
        double result = op1 - op2;
        if (op1 >= 0 && op2 <= 0) {
            // result has to be < 0 for overflow
            return result == Double.POSITIVE_INFINITY ? -1 : HALFWAY + 1
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            // if both are negative then an overflow will be difficult
            return result == Double.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1) + Math.abs(op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceSub(double op1, double op2) {
        double result = op1 - op2;
        if (op1 <= 0 && op2 >= 0) {
            return result == Double.NEGATIVE_INFINITY ? -1 : HALFWAY + 1
                    - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 < 0) {
            return result == Double.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1) + Math.abs(op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else {
            // Not sure if this can be reached
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int overflowDistanceMul(double op1, double op2) {
        double result = op1 * op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            return result == Double.POSITIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 < 0) {
            return result == Double.POSITIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY) + 1;
        } else if (op1 > 0 && op2 < 0) {
            // In this case we can't have an overflow yet
            return result == Double.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return result == Double.NEGATIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int underflowDistanceMul(double op1, double op2) {
        double result = op1 * op2;
        if (op1 > 0 && op2 < 0) {
            return result == Double.NEGATIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return result == Double.NEGATIVE_INFINITY ? -1 : HALFWAY
                    - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 > 0) {
            return result == Double.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.min(op1, op2), HALFWAY);
        } else if (op1 < 0 && op2 < 0) {
            return result == Double.POSITIVE_INFINITY ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(Math.max(op1, op2)), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int overflowDistanceDiv(double op1, double op2) {
        if (op1 == -Double.MAX_VALUE && op2 == -1.0)
            return -1;
        else
            // TODO There may be an overflow here
            return scaleTo(Math.abs(-Double.MAX_VALUE - op1), HALFWAY)
                    + scaleTo(Math.abs(-1.0 - op2), HALFWAY);
    }

    public static int overflowDistance(long op1, long op2, int opcode) {
        switch (opcode) {

            case Opcodes.LADD:
                return overflowDistanceAdd(op1, op2);

            case Opcodes.LSUB:
                return overflowDistanceSub(op1, op2);

            case Opcodes.LMUL:
                return overflowDistanceMul(op1, op2);

            case Opcodes.LDIV:
                return overflowDistanceDiv(op1, op2);
        }
        return Integer.MAX_VALUE;
    }

    public static int underflowDistance(long op1, long op2, int opcode) {
        switch (opcode) {

            case Opcodes.LADD:
                return underflowDistanceAdd(op1, op2);

            case Opcodes.LSUB:
                return underflowDistanceSub(op1, op2);

            case Opcodes.LMUL:
                return underflowDistanceMul(op1, op2);

        }
        return Integer.MAX_VALUE;
    }

    protected static int overflowDistanceAdd(long op1, long op2) {
        long result = op1 + op2;
        if (op1 > 0 && op2 > 0) {
            // result has to be < 0 for overflow
            return result < 0 ? -scaleTo(Math.abs(result), HALFWAY) : HALFWAY
                    - scaleTo(result, HALFWAY) + 1;

        } else if (op1 < 0 && op2 < 0) {
            return result > 0 ? Integer.MAX_VALUE : HALFWAY - scaleTo(result, HALFWAY)
                    + 1;
        } else if (op1 >= 0 && op2 < 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            // If only one is negative, then optimize that to be positive
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceAdd(long op1, long op2) {
        long result = op1 + op2;
        if (op1 <= 0L && op2 <= 0L) {
            // result has to be < 0 for underflow
            if (result > 0) {
                int retval = -scaleTo(result, HALFWAY);
                if (retval < 0)
                    return retval;
                else
                    return -1;
            } else if (result == 0L) {
                if (op1 != 0 && op2 != 0) {
                    return -1;
                } else {
                    return HALFWAY
                            - scaleTo(Math.abs(result), HALFWAY);
                }
            } else {
                int intResult = HALFWAY
                        - scaleTo(Math.abs(result), HALFWAY);
                if (intResult == 0)
                    return 1;
                else
                    return intResult;
            }
        } else if (op1 > 0 && op2 > 0) {
            // if both are positive then both need to be decreased
            return result < 0 ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1) + Math.abs(op2), HALFWAY);
        } else if (op1 >= 0 && op2 < 0) {
            return HALFWAY + scaleTo(op1, HALFWAY);
        } else if (op1 < 0 && op2 >= 0) {
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else {
            // Unreachable
            return Integer.MAX_VALUE;
        }
    }

    protected static int overflowDistanceSub(long op1, long op2) {
        long result = op1 - op2;
        if (op1 >= 0 && op2 <= 0) {
            // result has to be < 0 for overflow
            return result < 0 ? -scaleTo(Math.abs(result), HALFWAY) : HALFWAY + 1
                    - scaleTo(result, HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            // if both are negative then an overflow will be difficult
            return result > 0 ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1) + Math.abs(op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(op2, HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // At least one of them is zero, and the sum is larger or equals than 0
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int underflowDistanceSub(long op1, long op2) {
        long result = op1 - op2;
        if (op1 <= 0 && op2 >= 0) {
            return result > 0 ? -scaleTo(result, HALFWAY) : HALFWAY + 1
                    - scaleTo(result, HALFWAY);
        } else if (op1 > 0 && op2 < 0) {
            return result < 0 ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1) + Math.abs(op2), HALFWAY);
        } else if (op1 >= 0 && op2 > 0) {
            // In this case we can't have an overflow yet
            return HALFWAY + scaleTo(Math.abs(op1), HALFWAY);
        } else if (op1 < 0 && op2 <= 0) {
            return 1 + HALFWAY + scaleTo(Math.abs(op2), HALFWAY);
        } else {
            // Not sure if this can be reached
            return 1 + HALFWAY - scaleTo(result, HALFWAY);
        }
    }

    protected static int overflowDistanceMul(long op1, long op2) {
        long result = op1 * op2;
        if ((op1 > 0 && op2 > 0) || (op1 < 0 && op2 < 0)) {
            BigDecimal bigDecimal = new BigDecimal(op1).multiply(new BigDecimal(op2));
            BigDecimal maxResult = new BigDecimal(Long.MAX_VALUE);

            if (bigDecimal.compareTo(maxResult) > 0) {
                int intResult = -scaleTo(Math.abs(result), HALFWAY);
                if (result <= 0)
                    return intResult;
                else
                    return Integer.MIN_VALUE;
            } else {
                int retval = HALFWAY - scaleTo(result, HALFWAY);
                if (retval > 0)
                    return retval;
                else
                    return 1;
            }
            // result has to be < 0 for overflow
        } else if (op1 > 0 && op2 < 0) {
            // In this case we can't have an overflow yet
            return result > 0 ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op2), HALFWAY);
        } else if (op1 < 0 && op2 > 0) {
            return result > 0 ? Integer.MAX_VALUE : HALFWAY
                    + scaleTo(Math.abs(op1), HALFWAY);
        } else {
            // One of them is zero
            return HALFWAY;
        }
    }

    protected static int underflowDistanceMul(long op1, long op2) {
        long result = op1 * op2;
        BigDecimal bigDecimal = new BigDecimal(op1).multiply(new BigDecimal(op2));
        BigDecimal minResult = new BigDecimal(Long.MIN_VALUE);

        if (bigDecimal.compareTo(minResult) < 0) {
            int intResult = -scaleTo(Math.abs(result), HALFWAY);
            if (result <= 0)
                return intResult;
            else
                return Integer.MIN_VALUE;
        } else {
            int retval = HALFWAY - scaleTo(result, HALFWAY);
            if (retval > 0)
                return retval;
            else
                return 1;
        }
    }

    protected static int overflowDistanceDiv(long op1, long op2) {
        if (op1 == Long.MIN_VALUE && op2 == -1L)
            return -1;
        else
            // TODO There may be an overflow here
            return scaleTo(Math.abs(Long.MIN_VALUE - op1), HALFWAY)
                    + scaleTo(Math.abs(-1L - op2), HALFWAY);
    }

}
